<?php


namespace Login;


use app\common\lib\RespCode;
use custom\Curl;
use custom\Log;
use think\Exception;

class Jwt {

    protected static $supported_algs = [
        'HS256' => ['hash_hmac', 'SHA256'],
        'HS512' => ['hash_hmac', 'SHA512'],
        'HS384' => ['hash_hmac', 'SHA384'],
        'RS256' => ['openssl', 'SHA256'],
        'RS384' => ['openssl', 'SHA384'],
        'RS512' => ['openssl', 'SHA512'],
    ];
    protected static $curlConf = [
        'google' => 'https://www.googleapis.com/oauth2/v1/certs',
        'apple'  => 'https://appleid.apple.com/auth/keys'
    ];
    protected static $curl;
    protected static $fun;
    protected static $instance;

    protected function __construct() {

    }

    public static function getInstance() {
        if (!self::$instance instanceof self) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function setCurl($type) {
        self::$curl = self::$curlConf[$type] ?? false;
        self::$fun = $type;
        return self::$instance;
    }

    public function verifyToken($userID, $identityToken) {
        if (!self::$curl) {
            Log::write('未定义三方', 'jwt.log');
            throw new Exception('未定义三方', RespCode::PARAMS_ERR);
        }
        $token = explode('.', $identityToken);
        if(!isset($token[2])||!isset($token[1])) {
            Log::write('参数错误', 'jwt.log');
            throw new Exception('参数错误', RespCode::PARAMS_ERR);
        }
        $jwtHeader = json_decode(base64_decode($token[0]), TRUE);
        $jwtData = json_decode(base64_decode($token[1]), TRUE);
        $jwtSign = $token[2];
        if ($userID !== $jwtData['sub']) {
            Log::write('用户ID与token不对应', 'jwt.log');
            throw new Exception('用户ID与token不对应', RespCode::PARAMS_ERR);
        }
        if ($jwtData['exp'] < time()) {
            Log::write('token已过期，请重新登录', 'jwt.log');
            throw new Exception('token已过期，请重新登录', RespCode::PARAMS_ERR);
        }

        $pem = $this->{self::$fun}($jwtHeader);
        $pKey = openssl_pkey_get_public($pem);
        if ($pKey === FALSE) {
            Log::write('生成pem失败', 'jwt.log');
            throw new Exception('生成pem失败', RespCode::PARAMS_ERR);
        }
        $publicKeyDetails = openssl_pkey_get_details($pKey);
        $pub_key = $publicKeyDetails['key'];
        $alg = $jwtHeader['alg'];
        $ok = self::verify("$token[0].$token[1]", static::urlsafeB64Decode($jwtSign), $pub_key, $alg);
        if (!$ok) {
            Log::write('登录签名校验失败', 'jwt.log');
            throw new Exception('登录签名校验失败', RespCode::PARAMS_ERR);
        }
        return [
            'id'    => $jwtData['sub'],
            'email' => $jwtData['email'] ?? ''
        ];
    }

    protected function apple($jwtHeader) {
        $appleKeys = Curl::getInstance()->xhr("GET", self::$curl);
        $appleKeys = json_decode($appleKeys, TRUE);
        if (!$appleKeys) {
            Log::write('请求苹果服务器失败', 'jwt.log');
            return false;
        }

        $theAppleKey = [];
        foreach ($appleKeys['keys'] as $key) {
            if ($key['kid'] == $jwtHeader['kid']) {
                $theAppleKey = $key;
            }
        }

        return self::createPemFromModulusAndExponent($theAppleKey['n'], $theAppleKey['e']);
    }

    public function google($jwtHeader) {
        $googlePem = Curl::getInstance()->xhr("GET", self::$curl);
        $googlePem = json_decode($googlePem, TRUE);
        if (!$googlePem) {
            Log::write('请求Google服务器失败', 'jwt.log');
            return false;
        }

        if (empty($googlePem[$jwtHeader['kid']])) {
            Log::write('google解析pem证书失败', 'jwt.log');
            return false;
        }

        return $googlePem[$jwtHeader['kid']];
    }


    protected static function createPemFromModulusAndExponent($n, $e) {
        $modulus = static::urlsafeB64Decode($n);
        $publicExponent = static::urlsafeB64Decode($e);

        $components = [
            'modulus'        => pack('Ca*a*', 2, self::encodeLength(strlen($modulus)), $modulus),
            'publicExponent' => pack('Ca*a*', 2, self::encodeLength(strlen($publicExponent)), $publicExponent)
        ];

        $RSAPublicKey = pack(
            'Ca*a*a*',
            48,
            self::encodeLength(strlen($components['modulus']) + strlen($components['publicExponent'])),
            $components['modulus'],
            $components['publicExponent']
        );

        // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption.
        $rsaOID = pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA
        $RSAPublicKey = chr(0) . $RSAPublicKey;
        $RSAPublicKey = chr(3) . self::encodeLength(strlen($RSAPublicKey)) . $RSAPublicKey;

        $RSAPublicKey = pack(
            'Ca*a*',
            48,
            self::encodeLength(strlen($rsaOID . $RSAPublicKey)),
            $rsaOID . $RSAPublicKey
        );

        $RSAPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" .
            chunk_split(base64_encode($RSAPublicKey), 64) .
            '-----END PUBLIC KEY-----';

        return $RSAPublicKey;
    }



    protected static function urlsafeB64Decode($input) {
        $remainder = strlen($input) % 4;
        if ($remainder) {
            $padlen = 4 - $remainder;
            $input .= str_repeat('=', $padlen);
        }
        return base64_decode(strtr($input, '-_', '+/'));
    }


    protected static function encodeLength($length) {
        if ($length <= 0x7F) {
            return chr($length);
        }

        $temp = ltrim(pack('N', $length), chr(0));
        return pack('Ca*', 0x80 | strlen($temp), $temp);
    }


    protected static function safeStrlen($str) {
        if (function_exists('mb_strlen')) {
            return mb_strlen($str, '8bit');
        }
        return strlen($str);
    }


    protected static function verify($msg, $signature, $key, $alg) {
        if (empty(static::$supported_algs[$alg])) {
            throw new DomainException('Algorithm not supported');
        }

        list($function, $algorithm) = static::$supported_algs[$alg];
        switch ($function) {
            case 'openssl':
                $success = openssl_verify($msg, $signature, $key, $algorithm);
                if ($success === 1) {
                    return true;
                } elseif ($success === 0) {
                    return false;
                }
                // returns 1 on success, 0 on failure, -1 on error.
                throw new DomainException(
                    'OpenSSL error: ' . openssl_error_string()
                );
            case 'hash_hmac':
            default:
                $hash = hash_hmac($algorithm, $msg, $key, true);
                if (function_exists('hash_equals')) {
                    return hash_equals($signature, $hash);
                }
                $len = min(static::safeStrlen($signature), static::safeStrlen($hash));

                $status = 0;
                for ($i = 0; $i < $len; $i++) {
                    $status |= (ord($signature[$i]) ^ ord($hash[$i]));
                }
                $status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash));

                return ($status === 0);
        }
    }

}
